"""
Sample from a trained model

generate `num_samples` samples with the length of `max_new_tokens` for each sample.

对于每一个sample：
从Transformer中采样，需要输入原始文本。
例如，输入长度为200的原始文本，经过encoder，得到k，v，然后在decoder中输入标签（第一次输入的是prompt，可以简单地输入'/n'）。
然后每次都得到一个输出，再将输出拼接，再次输入到decoder中, ...重复max_new_tokens次。

这里需要注意的是，原始文本的长度可以自定，可以很长（全样本数据），也可以很短。这个取决于具体情况。
不过建议原始文本长度和max_new_tokens相同。因为训练的时候用的样本和标签的长度是相同的(都是T)，
则对于长度为max_new_tokens的原始文本，我们期望得到的输出长度也是num_samples，符合采样目标。

综上，对每个sample，先原始文本中随机采样长度为max_new_tokens的连续段落，然后经过transformer模型的推理，得到一个长度为max_new_tokens的输出
"""
import os
from contextlib import nullcontext
import torch
import tiktoken
from model import GPTConfig, GPT
from data import get_batch, OUTPUT_PATH, RAW_DATA_PATH

# -----------------------------------------------------------------------------
data_name = ''  # need this to get k, v from encoder
init_from = 'resume'  # either 'resume' (from an out_dir) or a gpt2 variant (e.g. 'gpt2-xl')
model_name = 'transformer'
encode_method = 'gpt2'
start = "\n"  # or "<|endoftext|>" or etc. Can also specify a file, use as: "FILE:prompt.txt"
num_samples = 10  # number of samples to draw
max_new_tokens = 500  # number of tokens generated in each sample
temperature = 0.8  # 1.0 = no change, < 1.0 = less random, > 1.0 = more random, in predictions
top_k = 200  # retain only the top_k most likely tokens, clamp others to have 0 probability
seed = 1337
device = 'mps'  # examples: 'cpu', 'cuda', 'cuda:0', 'cuda:1', etc.
dtype = 'bfloat16' if torch.cuda.is_available() and torch.cuda.is_bf16_supported() else 'float16'  # 'float32' or 'bfloat16' or 'float16'
compile = False  # use PyTorch 2.0 to compile the model to be faster
exec(open('../configurator.py').read())  # overrides from command line or config file
# -----------------------------------------------------------------------------

torch.manual_seed(seed)
torch.cuda.manual_seed(seed)
torch.backends.cuda.matmul.allow_tf32 = True  # allow tf32 on matmul
torch.backends.cudnn.allow_tf32 = True  # allow tf32 on cudnn
device_type = 'cuda' if 'cuda' in device else 'cpu'  # for later use in torch.autocast
ptdtype = {'float32': torch.float32, 'bfloat16': torch.bfloat16, 'float16': torch.float16}[dtype]
ctx = nullcontext() if device_type == 'cpu' else torch.amp.autocast(device_type=device_type, dtype=ptdtype)

# model
if init_from == 'resume':
    # init from a model saved in a specific directory
    ckpt_path = os.path.join(OUTPUT_PATH, f'{model_name}_ckpt.pt')
    assert os.path.exists(ckpt_path), f'Model {ckpt_path} does not exist.'
    checkpoint = torch.load(ckpt_path, map_location=device)
    gptconf = GPTConfig(**checkpoint['model_args'])
    model = GPT(gptconf)
    state_dict = checkpoint['model']
    unwanted_prefix = '_orig_mod.'
    for k, v in list(state_dict.items()):
        if k.startswith(unwanted_prefix):
            state_dict[k[len(unwanted_prefix):]] = state_dict.pop(k)
    model.load_state_dict(state_dict)
elif init_from.startswith('gpt2'):
    # init from a given GPT-2 model
    model = GPT.from_pretrained(init_from, dict(dropout=0.0))
else:
    raise ValueError

model.eval()
model.to(device)
if compile:
    model = torch.compile(model)  # requires PyTorch 2.0 (optional)

if encode_method == 'gpt2':
    # let's assume gpt-2 encodings by default
    print("Assuming GPT-2 encodings...")
    enc = tiktoken.get_encoding("gpt2")
    encode = lambda s: enc.encode(s, allowed_special={"<|endoftext|>"})
    decode = lambda l: enc.decode(l)
elif encode_method == 'character':
    input_file_path = os.path.join(RAW_DATA_PATH, 'input.txt')
    with open(input_file_path, 'r', encoding='utf-8') as f:
        text = f.read()
    chars = sorted(list(set(text)))
    # create a mapping from characters to integers
    stoi = {ch: i for i, ch in enumerate(chars)}
    itos = {i: ch for i, ch in enumerate(chars)}
    # encoder: take a string, output a list of integers
    encode = lambda s: [stoi[c] for c in s]
    # decoder: take a list of integers, output a string
    decode = lambda l: ''.join([itos[i] for i in l])

else:
    raise NotImplementedError(f'Does not support encoding method {encode_method}.')

# encode the beginning of the prompt
# start就理解为prompt
if start.startswith('FILE:'):
    with open(start[5:], 'r', encoding='utf-8') as f:
        start = f.read()
start_ids = encode(start)  # 默认的start_ids是0，代表'/n'编码后的结果。
x = (torch.tensor(start_ids, dtype=torch.long, device=device)[None, ...])  # tensor([[0]])

# a reasonable method: randomly sampling a text with length blck_size as the source for encoder
block_size = max_new_tokens
# source is the input for encoder
source, _ = get_batch(block_size, batch_size=1, split='train', data_name=data_name, device=device)

# run generation
with torch.no_grad():
    with ctx:
        for k in range(num_samples):
            y = model.generate(source, x, max_new_tokens, temperature=temperature, top_k=top_k)
            print(decode(y[0].tolist()))  # length of every sample is max_new_tokens
            print('---------------')
